import { Webgl2DBase } from "../webgl/Webgl2DBase";

/*
 * @Author: Snow
 * @Date: 2021-09-28 21:09:12
 * @Description: webgl工具类
 */
export class WebGLUtil {
	/**
	 * @Author: Snow
	 * @description: 创建WebGL程序
	 * @param {WebGLRenderingContext} gl
	 * @param {string} vsScript 顶点着色器GLSL脚本
	 * @param {string} fsScript 片段着色器GLSL脚本
	 * @return {*}
	 */
	public static createProgramFromScripts(gl: WebGLRenderingContext, vsScript: string, fsScript: string): WebGLProgram {
		let vertexShader = this.compileShader(gl, vsScript, gl.VERTEX_SHADER);
		let fragmentShader = this.compileShader(gl, fsScript, gl.FRAGMENT_SHADER);
		return this.createProgram(gl, vertexShader, fragmentShader);
	}

	/**
	* 创建并编译一个着色器
	*
	* @param {!WebGLRenderingContext} gl WebGL上下文。
	* @param {string} shaderSource GLSL 格式的着色器代码
	* @param {number} shaderType 着色器类型, VERTEX_SHADER 或
	*     FRAGMENT_SHADER。
	* @return {!WebGLShader} 着色器。
	*/
	private static compileShader(gl: WebGLRenderingContext, shaderSource: string, shaderType: number) {
		// 创建着色器程序
		let shader = gl.createShader(shaderType);

		// 设置着色器的源码
		gl.shaderSource(shader, shaderSource);

		// 编译着色器
		gl.compileShader(shader);

		// 检测编译是否成功
		let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
		if (!success) {
			const lastError = gl.getShaderInfoLog(shader);
			console.error('*** Error compiling shader \'' + shader + '\':' + lastError + `\n` + shaderSource.split('\n').map((l, i) => `${i + 1}: ${l}`).join('\n'));
			// 编译过程出错，获取错误信息。
			throw "could not compile shader:" + lastError;
		}

		return shader;
	}

	/**
	* 从 2 个着色器中创建一个程序
	*
	* @param {!WebGLRenderingContext) gl WebGL上下文。
	* @param {!WebGLShader} vertexShader 一个顶点着色器。
	* @param {!WebGLShader} fragmentShader 一个片断着色器。
	* @return {!WebGLProgram} 程序
	*/
	private static createProgram(gl, vertexShader, fragmentShader) {
		// 创建一个程序
		var program = gl.createProgram();

		// 附上着色器
		gl.attachShader(program, vertexShader);
		gl.attachShader(program, fragmentShader);

		// 链接到程序
		gl.linkProgram(program);

		// 检查链接是否成功
		var success = gl.getProgramParameter(program, gl.LINK_STATUS);
		if (!success) {
			// 链接过程出现问题
			throw ("program failed to link:" + gl.getProgramInfoLog(program));
		}
		return program;
	};

	/**
	 * @Author: Snow
	 * @description: 适配屏幕画布大小
	 * @param {WebGLRenderingContext} gl
	 * @param {HTMLCanvasElement} canvas
	 * @return {*}
	 */
	public static resizeCanvasToMatchDisplaySize(gl: WebGLRenderingContext):boolean {
		// 获取浏览器中画布的显示尺寸
		let displayWidth = gl.canvas.clientWidth;
		let displayHeight = gl.canvas.clientHeight;
		let isResize = false;
		// 检尺寸是否相同
		if (gl.canvas.width != displayWidth ||
			gl.canvas.height != displayHeight) {

			// 设置为相同的尺寸
			gl.canvas.width = displayWidth;
			gl.canvas.height = displayHeight;
			isResize = true;
		}
		gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
		return isResize;
	}


	/**
	 * Returns the corresponding bind point for a given sampler type
	 */
	public static getBindPointForSamplerType(gl: WebGLRenderingContext, type: number): number {
		if (type === gl.SAMPLER_2D) return gl.TEXTURE_2D;        // eslint-disable-line
		if (type === gl.SAMPLER_CUBE) return gl.TEXTURE_CUBE_MAP;  // eslint-disable-line
		return undefined;
	}

	/**
	 * Creates setter functions for all uniforms of a shader
	 * program.
	 *
	 * @see {@link module:webgl-utils.setUniforms}
	 *
	 * @param {WebGLProgram} program the program to create setters for.
	 * @returns {Object.<string, function>} an object with a setter by name for each uniform
	 * @memberOf module:webgl-utils
	 */
	public static createUniformSetters(gl: WebGLRenderingContext, program: WebGLProgram): { [name: string]: Function } {
		let textureUnit = 0;

		/**
		 * Creates a setter for a uniform of the given program with it's
		 * location embedded in the setter.
		 * @param {WebGLProgram} program
		 * @param {WebGLUniformInfo} uniformInfo
		 * @returns {function} the created setter.
		 */
		function createUniformSetter(program: WebGLProgram, uniformInfo: WebGLActiveInfo): Function {
			const location = gl.getUniformLocation(program, uniformInfo.name);
			const type = uniformInfo.type;
			// Check if this uniform is an array
			const isArray = (uniformInfo.size > 1 && uniformInfo.name.substr(-3) === '[0]');
			if (type === gl.FLOAT && isArray) {
				return function (v) {
					gl.uniform1fv(location, v);
				};
			}
			if (type === gl.FLOAT) {
				return function (v) {
					gl.uniform1f(location, v);
				};
			}
			if (type === gl.FLOAT_VEC2) {
				return function (v) {
					gl.uniform2fv(location, v);
				};
			}
			if (type === gl.FLOAT_VEC3) {
				return function (v) {
					gl.uniform3fv(location, v);
				};
			}
			if (type === gl.FLOAT_VEC4) {
				return function (v) {
					gl.uniform4fv(location, v);
				};
			}
			if (type === gl.INT && isArray) {
				return function (v) {
					gl.uniform1iv(location, v);
				};
			}
			if (type === gl.INT) {
				return function (v) {
					gl.uniform1i(location, v);
				};
			}
			if (type === gl.INT_VEC2) {
				return function (v) {
					gl.uniform2iv(location, v);
				};
			}
			if (type === gl.INT_VEC3) {
				return function (v) {
					gl.uniform3iv(location, v);
				};
			}
			if (type === gl.INT_VEC4) {
				return function (v) {
					gl.uniform4iv(location, v);
				};
			}
			if (type === gl.BOOL) {
				return function (v) {
					gl.uniform1iv(location, v);
				};
			}
			if (type === gl.BOOL_VEC2) {
				return function (v) {
					gl.uniform2iv(location, v);
				};
			}
			if (type === gl.BOOL_VEC3) {
				return function (v) {
					gl.uniform3iv(location, v);
				};
			}
			if (type === gl.BOOL_VEC4) {
				return function (v) {
					gl.uniform4iv(location, v);
				};
			}
			if (type === gl.FLOAT_MAT2) {
				return function (v) {
					gl.uniformMatrix2fv(location, false, v);
				};
			}
			if (type === gl.FLOAT_MAT3) {
				return function (v) {
					gl.uniformMatrix3fv(location, false, v);
				};
			}
			if (type === gl.FLOAT_MAT4) {
				return function (v) {
					// console.log(gl)
					// console.log(location)
					// console.log(uniformInfo.name)
					// console.log(v)
					gl.uniformMatrix4fv(location, false, v);
				};
			}
			if ((type === gl.SAMPLER_2D || type === gl.SAMPLER_CUBE) && isArray) {
				const units = [];
				for (let ii = 0; ii < uniformInfo.size; ++ii) {
					units.push(textureUnit++);
				}
				return function (bindPoint, units) {
					return function (textures) {
						gl.uniform1iv(location, units);
						textures.forEach(function (texture, index) {
							gl.activeTexture(gl.TEXTURE0 + units[index]);
							gl.bindTexture(bindPoint, texture);
						});
					};
				}(WebGLUtil.getBindPointForSamplerType(gl, type), units);
			}
			if (type === gl.SAMPLER_2D || type === gl.SAMPLER_CUBE) {
				return function (bindPoint, unit) {
					return function (texture) {
						gl.uniform1i(location, unit);
						gl.activeTexture(gl.TEXTURE0 + unit);
						gl.bindTexture(bindPoint, texture);
					};
				}(WebGLUtil.getBindPointForSamplerType(gl, type), textureUnit++);
			}
			throw ('unknown type: 0x' + type.toString(16)); // we should never get here.
		}

		const uniformSetters = {};
		const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);

		for (let ii = 0; ii < numUniforms; ++ii) {
			const uniformInfo: WebGLActiveInfo = gl.getActiveUniform(program, ii);
			if (!uniformInfo) {
				break;
			}
			let name = uniformInfo.name;
			// remove the array suffix.
			if (name.substr(-3) === '[0]') {
				name = name.substr(0, name.length - 3);
			}
			const setter = createUniformSetter(program, uniformInfo);
			uniformSetters[name] = setter;
		}
		return uniformSetters;
	}

	/**
	 * Set uniforms and binds related textures.
	 *
	 * Example:
	 *
	 *     let programInfo = createProgramInfo(
	 *         gl, ["some-vs", "some-fs"]);
	 *
	 *     let tex1 = gl.createTexture();
	 *     let tex2 = gl.createTexture();
	 *
	 *     ... assume we setup the textures with data ...
	 *
	 *     let uniforms = {
	 *       u_someSampler: tex1,
	 *       u_someOtherSampler: tex2,
	 *       u_someColor: [1,0,0,1],
	 *       u_somePosition: [0,1,1],
	 *       u_someMatrix: [
	 *         1,0,0,0,
	 *         0,1,0,0,
	 *         0,0,1,0,
	 *         0,0,0,0,
	 *       ],
	 *     };
	 *
	 *     gl.useProgram(program);
	 *
	 * This will automatically bind the textures AND set the
	 * uniforms.
	 *
	 *     setUniforms(programInfo.uniformSetters, uniforms);
	 *
	 * For the example above it is equivalent to
	 *
	 *     let texUnit = 0;
	 *     gl.activeTexture(gl.TEXTURE0 + texUnit);
	 *     gl.bindTexture(gl.TEXTURE_2D, tex1);
	 *     gl.uniform1i(u_someSamplerLocation, texUnit++);
	 *     gl.activeTexture(gl.TEXTURE0 + texUnit);
	 *     gl.bindTexture(gl.TEXTURE_2D, tex2);
	 *     gl.uniform1i(u_someSamplerLocation, texUnit++);
	 *     gl.uniform4fv(u_someColorLocation, [1, 0, 0, 1]);
	 *     gl.uniform3fv(u_somePositionLocation, [0, 1, 1]);
	 *     gl.uniformMatrix4fv(u_someMatrix, false, [
	 *         1,0,0,0,
	 *         0,1,0,0,
	 *         0,0,1,0,
	 *         0,0,0,0,
	 *       ]);
	 *
	 * Note it is perfectly reasonable to call `setUniforms` multiple times. For example
	 *
	 *     let uniforms = {
	 *       u_someSampler: tex1,
	 *       u_someOtherSampler: tex2,
	 *     };
	 *
	 *     let moreUniforms {
	 *       u_someColor: [1,0,0,1],
	 *       u_somePosition: [0,1,1],
	 *       u_someMatrix: [
	 *         1,0,0,0,
	 *         0,1,0,0,
	 *         0,0,1,0,
	 *         0,0,0,0,
	 *       ],
	 *     };
	 *
	 *     setUniforms(programInfo.uniformSetters, uniforms);
	 *     setUniforms(programInfo.uniformSetters, moreUniforms);
	 *
	 * @param {Object.<string, function>|module:webgl-utils.ProgramInfo} setters the setters returned from
	 *        `createUniformSetters` or a ProgramInfo from {@link module:webgl-utils.createProgramInfo}.
	 * @param {Object.<string, value>} an object with values for the
	 *        uniforms.
	 * @memberOf module:webgl-utils
	 */
	public static setUniforms(setters: any, ...values) {
		setters = setters.uniformSetters || setters;
		for (const uniforms of values) {
			Object.keys(uniforms).forEach(function (name) {
				const setter = setters[name];
				if (setter) {
					setter(uniforms[name]);
				}
			});
		}
	}

	/**
	 * Creates setter functions for all attributes of a shader
	 * program. You can pass this to {@link module:webgl-utils.setBuffersAndAttributes} to set all your buffers and attributes.
	 *
	 * @see {@link module:webgl-utils.setAttributes} for example
	 * @param {WebGLProgram} program the program to create setters for.
	 * @return {Object.<string, function>} an object with a setter for each attribute by name.
	 * @memberOf module:webgl-utils
	 */
	public static createAttributeSetters(gl: WebGLRenderingContext, program: WebGLProgram) {
		const attribSetters = {
		};

		function createAttribSetter(index) {
			return function (b) {
				if (b.value) {
					gl.disableVertexAttribArray(index);
					switch (b.value.length) {
						case 4:
							gl.vertexAttrib4fv(index, b.value);
							break;
						case 3:
							gl.vertexAttrib3fv(index, b.value);
							break;
						case 2:
							gl.vertexAttrib2fv(index, b.value);
							break;
						case 1:
							gl.vertexAttrib1fv(index, b.value);
							break;
						default:
							throw new Error('the length of a float constant value must be between 1 and 4!');
					}
				} else {
					gl.bindBuffer(gl.ARRAY_BUFFER, b.buffer);
					gl.enableVertexAttribArray(index);
					gl.vertexAttribPointer(
						index, b.numComponents || b.size, b.type || gl.FLOAT, b.normalize || false, b.stride || 0, b.offset || 0);
				}
			};
		}

		const numAttribs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
		for (let ii = 0; ii < numAttribs; ++ii) {
			const attribInfo: WebGLActiveInfo = gl.getActiveAttrib(program, ii);
			if (!attribInfo) {
				break;
			}
			const index = gl.getAttribLocation(program, attribInfo.name);
			attribSetters[attribInfo.name] = createAttribSetter(index);
		}

		return attribSetters;
	}

	/**
	 * Sets attributes and binds buffers (deprecated... use {@link module:webgl-utils.setBuffersAndAttributes})
	 *
	 * Example:
	 *
	 *     let program = createProgramFromScripts(
	 *         gl, ["some-vs", "some-fs"]);
	 *
	 *     let attribSetters = createAttributeSetters(program);
	 *
	 *     let positionBuffer = gl.createBuffer();
	 *     let texcoordBuffer = gl.createBuffer();
	 *
	 *     let attribs = {
	 *       a_position: {buffer: positionBuffer, numComponents: 3},
	 *       a_texcoord: {buffer: texcoordBuffer, numComponents: 2},
	 *     };
	 *
	 *     gl.useProgram(program);
	 *
	 * This will automatically bind the buffers AND set the
	 * attributes.
	 *
	 *     setAttributes(attribSetters, attribs);
	 *
	 * Properties of attribs. For each attrib you can add
	 * properties:
	 *
	 * *   type: the type of data in the buffer. Default = gl.FLOAT
	 * *   normalize: whether or not to normalize the data. Default = false
	 * *   stride: the stride. Default = 0
	 * *   offset: offset into the buffer. Default = 0
	 *
	 * For example if you had 3 value float positions, 2 value
	 * float texcoord and 4 value uint8 colors you'd setup your
	 * attribs like this
	 *
	 *     let attribs = {
	 *       a_position: {buffer: positionBuffer, numComponents: 3},
	 *       a_texcoord: {buffer: texcoordBuffer, numComponents: 2},
	 *       a_color: {
	 *         buffer: colorBuffer,
	 *         numComponents: 4,
	 *         type: gl.UNSIGNED_BYTE,
	 *         normalize: true,
	 *       },
	 *     };
	 *
	 * @param {Object.<string, function>|model:webgl-utils.ProgramInfo} setters Attribute setters as returned from createAttributeSetters or a ProgramInfo as returned {@link module:webgl-utils.createProgramInfo}
	 * @param {Object.<string, module:webgl-utils.AttribInfo>} attribs AttribInfos mapped by attribute name.
	 * @memberOf module:webgl-utils
	 * @deprecated use {@link module:webgl-utils.setBuffersAndAttributes}
	 */
	public static setAttributes(setters, attribs): void {
		setters = setters.attribSetters || setters;
		Object.keys(attribs).forEach(function (name) {
			const setter = setters[name];
			if (setter) {
				setter(attribs[name]);
			}
		});
	}

	/**
	 * @Author: Snow
	 * @description: 创建程序信息
	 * @param {WebGLRenderingContext} gl
	 * @param {string} vsSource
	 * @param {string} fsSource
	 * @return {*}
	 */
	public static createProgramInfo(gl: WebGLRenderingContext, vsSource: string, fsSource: string): {
		program: WebGLProgram,
		uniformSetters: { [name: string]: Function },
		attribSetters: { [name: string]: Function },
	} {
		const program = this.createProgramFromScripts(gl, vsSource, fsSource);
		if (!program) {
			return null;
		}
		const uniformSetters = this.createUniformSetters(gl, program);
		const attribSetters = this.createAttributeSetters(gl, program);
		return {
			program: program,
			uniformSetters: uniformSetters,
			attribSetters: attribSetters,
		};
	}

	/**
	 * Sets attributes and buffers including the `ELEMENT_ARRAY_BUFFER` if appropriate
	 *
	 * Example:
	 *
	 *     let programInfo = createProgramInfo(
	 *         gl, ["some-vs", "some-fs"]);
	 *
	 *     let arrays = {
	 *       position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], },
	 *       texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1],                 },
	 *     };
	 *
	 *     let bufferInfo = createBufferInfoFromArrays(gl, arrays);
	 *
	 *     gl.useProgram(programInfo.program);
	 *
	 * This will automatically bind the buffers AND set the
	 * attributes.
	 *
	 *     setBuffersAndAttributes(programInfo.attribSetters, bufferInfo);
	 *
	 * For the example above it is equivilent to
	 *
	 *     gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
	 *     gl.enableVertexAttribArray(a_positionLocation);
	 *     gl.vertexAttribPointer(a_positionLocation, 3, gl.FLOAT, false, 0, 0);
	 *     gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
	 *     gl.enableVertexAttribArray(a_texcoordLocation);
	 *     gl.vertexAttribPointer(a_texcoordLocation, 4, gl.FLOAT, false, 0, 0);
	 *
	 * @param {WebGLRenderingContext} gl A WebGLRenderingContext.
	 * @param {Object.<string, function>} setters Attribute setters as returned from `createAttributeSetters`
	 * @param {module:webgl-utils.BufferInfo} buffers a BufferInfo as returned from `createBufferInfoFromArrays`.
	 * @memberOf module:webgl-utils
	 */
	public static setBuffersAndAttributes(gl, setters, buffers) {
		this.setAttributes(setters, buffers.attribs);
		if (buffers.indices) {
			gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
		}
	}

	// Add `push` to a typed array. It just keeps a 'cursor'
	// and allows use to `push` values into the array so we
	// don't have to manually compute offsets
	private static augmentTypedArray(typedArray, numComponents) {
		let cursor = 0;
		typedArray.push = function () {
			for (let ii = 0; ii < arguments.length; ++ii) {
				const value = arguments[ii];
				if (value instanceof Array || (value.buffer && value.buffer instanceof ArrayBuffer)) {
					for (let jj = 0; jj < value.length; ++jj) {
						typedArray[cursor++] = value[jj];
					}
				} else {
					typedArray[cursor++] = value;
				}
			}
		};
		typedArray.reset = function (opt_index) {
			cursor = opt_index || 0;
		};
		typedArray.numComponents = numComponents;
		Object.defineProperty(typedArray, 'numElements', {
			get: function () {
				return this.length / this.numComponents | 0;
			},
		});
		return typedArray;
	}

	/**
   * creates a typed array with a `push` function attached
   * so that you can easily *push* values.
   *
   * `push` can take multiple arguments. If an argument is an array each element
   * of the array will be added to the typed array.
   *
   * Example:
   *
   *     let array = createAugmentedTypedArray(3, 2);  // creates a Float32Array with 6 values
   *     array.push(1, 2, 3);
   *     array.push([4, 5, 6]);
   *     // array now contains [1, 2, 3, 4, 5, 6]
   *
   * Also has `numComponents` and `numElements` properties.
   *
   * @param {number} numComponents number of components
   * @param {number} numElements number of elements. The total size of the array will be `numComponents * numElements`.
   * @param {constructor} opt_type A constructor for the type. Default = `Float32Array`.
   * @return {ArrayBuffer} A typed array.
   * @memberOf module:webgl-utils
   */
	public static createAugmentedTypedArray(numComponents: number, numElements: number, opt_type?:any): any {
		const Type = opt_type || Float32Array;
		return this.augmentTypedArray(new Type(numComponents * numElements), numComponents);
	}

	private static createBufferFromTypedArray(gl: WebGLRenderingContext, array: any, type?: number, drawType?: number) {
		type = type || gl.ARRAY_BUFFER;
		const buffer = gl.createBuffer();
		gl.bindBuffer(type, buffer);
		gl.bufferData(type, array, drawType || gl.STATIC_DRAW);
		return buffer;
	}

	private static allButIndices(name): boolean {
		return name !== 'indices';
	}

	private static createMapping(obj: any): any {
		const mapping = {};
		Object.keys(obj).filter(this.allButIndices).forEach(function (key) {
			mapping['a_' + key] = key;
		});
		return mapping;
	}

	private static getGLTypeForTypedArray(gl: WebGLRenderingContext, typedArray: any): number {
		if (typedArray instanceof Int8Array) { return gl.BYTE; }            // eslint-disable-line
		if (typedArray instanceof Uint8Array) { return gl.UNSIGNED_BYTE; }   // eslint-disable-line
		if (typedArray instanceof Int16Array) { return gl.SHORT; }           // eslint-disable-line
		if (typedArray instanceof Uint16Array) { return gl.UNSIGNED_SHORT; }  // eslint-disable-line
		if (typedArray instanceof Int32Array) { return gl.INT; }             // eslint-disable-line
		if (typedArray instanceof Uint32Array) { return gl.UNSIGNED_INT; }    // eslint-disable-line
		if (typedArray instanceof Float32Array) { return gl.FLOAT; }           // eslint-disable-line
		throw 'unsupported typed array type';
	}

	// This is really just a guess. Though I can't really imagine using
	// anything else? Maybe for some compression?
	private static getNormalizationForTypedArray(typedArray: any): boolean {
		if (typedArray instanceof Int8Array) { return true; }  // eslint-disable-line
		if (typedArray instanceof Uint8Array) { return true; }  // eslint-disable-line
		return false;
	}

	private static isArrayBuffer(a: { buffer: any }): boolean {
		return a.buffer && a.buffer instanceof ArrayBuffer;
	}

	// private static guessNumComponentsFromName(name: string, length: number): number {
	// 	let numComponents;
	// 	if (name.indexOf('coord') >= 0) {
	// 		numComponents = 2;
	// 	} else if (name.indexOf('color') >= 0) {
	// 		numComponents = 4;
	// 	} else {
	// 		numComponents = 3;  // position, normals, indices ...
	// 	}

	// 	if (length % numComponents > 0) {
	// 		throw 'can not guess numComponents. You should specify it.';
	// 	}

	// 	return numComponents;
	// }

	private static makeTypedArray(array: any, name?: string): { buffer: ArrayBuffer,
		numComponents?:number
	} {
		if (this.isArrayBuffer(array)) {
			return array;
		}

		if (array.data && this.isArrayBuffer(array.data)) {
			return array.data;
		}

		if (Array.isArray(array)) {
			array = {
				data: array,
			};
		}

		if (!array.numComponents) {
			array.numComponents = this.guessNumComponentsFromName(name, array.length);
		}

		let type = array.type;
		if (!type) {
			if (name === 'indices') {
				type = Uint16Array;
			}
		}
		const typedArray = this.createAugmentedTypedArray(array.numComponents, array.data.length / array.numComponents | 0, type);
		typedArray.push(array.data);
		return typedArray;
	}

	/**
	 * @typedef {Object} AttribInfo
	 * @property {number} [numComponents] the number of components for this attribute.
	 * @property {number} [size] the number of components for this attribute.
	 * @property {number} [type] the type of the attribute (eg. `gl.FLOAT`, `gl.UNSIGNED_BYTE`, etc...) Default = `gl.FLOAT`
	 * @property {boolean} [normalized] whether or not to normalize the data. Default = false
	 * @property {number} [offset] offset into buffer in bytes. Default = 0
	 * @property {number} [stride] the stride in bytes per element. Default = 0
	 * @property {WebGLBuffer} buffer the buffer that contains the data for this attribute
	 * @memberOf module:webgl-utils
	 */


	/**
	 * Creates a set of attribute data and WebGLBuffers from set of arrays
	 *
	 * Given
	 *
	 *      let arrays = {
	 *        position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], },
	 *        texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1],                 },
	 *        normal:   { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],     },
	 *        color:    { numComponents: 4, data: [255, 255, 255, 255, 255, 0, 0, 255, 0, 0, 255, 255], type: Uint8Array, },
	 *        indices:  { numComponents: 3, data: [0, 1, 2, 1, 2, 3],                       },
	 *      };
	 *
	 * returns something like
	 *
	 *      let attribs = {
	 *        a_position: { numComponents: 3, type: gl.FLOAT,         normalize: false, buffer: WebGLBuffer, },
	 *        a_texcoord: { numComponents: 2, type: gl.FLOAT,         normalize: false, buffer: WebGLBuffer, },
	 *        a_normal:   { numComponents: 3, type: gl.FLOAT,         normalize: false, buffer: WebGLBuffer, },
	 *        a_color:    { numComponents: 4, type: gl.UNSIGNED_BYTE, normalize: true,  buffer: WebGLBuffer, },
	 *      };
	 *
	 * @param {WebGLRenderingContext} gl The webgl rendering context.
	 * @param {Object.<string, array|typedarray>} arrays The arrays
	 * @param {Object.<string, string>} [opt_mapping] mapping from attribute name to array name.
	 *     if not specified defaults to "a_name" -> "name".
	 * @return {Object.<string, module:webgl-utils.AttribInfo>} the attribs
	 * @memberOf module:webgl-utils
	 */
	private static createAttribsFromArrays(gl: WebGLRenderingContext, arrays: any, opt_mapping?: any) {
		const mapping = opt_mapping || this.createMapping(arrays);
		const attribs = {};
		Object.keys(mapping).forEach(function (attribName) {
			const bufferName = mapping[attribName];
			const origArray = arrays[bufferName];
			if (origArray.value) {
				attribs[attribName] = {
					value: origArray.value,
				};
			} else {
				const array = WebGLUtil.makeTypedArray(origArray, bufferName);
				attribs[attribName] = {
					buffer: WebGLUtil.createBufferFromTypedArray(gl, array),
					numComponents: origArray.numComponents || array.numComponents || WebGLUtil.guessNumComponentsFromName(bufferName),
					type: WebGLUtil.getGLTypeForTypedArray(gl, array),
					normalize: WebGLUtil.getNormalizationForTypedArray(array),
				};
			}
		});
		return attribs;
	}

	private static getArray(array: any): any {
		return array.length ? array : array.data;
	}



	private static guessNumComponentsFromName(name: string, length?: number): number {
		const texcoordRE = /coord|texture/i;
		const colorRE = /color|colour/i;
		let numComponents;
		if (texcoordRE.test(name)) {
			numComponents = 2;
		} else if (colorRE.test(name)) {
			numComponents = 4;
		} else {
			numComponents = 3;  // position, normals, indices ...
		}

		if (length % numComponents > 0) {
			throw new Error(`Can not guess numComponents for attribute '${name}'. Tried ${numComponents} but ${length} values is not evenly divisible by ${numComponents}. You should specify it.`);
		}

		return numComponents;
	}

	private static getNumComponents(array, arrayName) {
		return array.numComponents || array.size || this.guessNumComponentsFromName(arrayName, this.getArray(array).length);
	}

	/**
	 * tries to get the number of elements from a set of arrays.
	 */
	private static getNumElementsFromNonIndexedArrays(arrays) {
		const positionKeys = ['position', 'positions', 'a_position'];
		let key;
		for (const k of positionKeys) {
			if (k in arrays) {
				key = k;
				break;
			}
		}
		key = key || Object.keys(arrays)[0];
		const array = arrays[key];
		const length = this.getArray(array).length;
		const numComponents = this.getNumComponents(array, key);
		const numElements = length / numComponents;
		if (length % numComponents > 0) {
			throw new Error(`numComponents ${numComponents} not correct for length ${length}`);
		}
		return numElements;
	}

	/**
	 * @typedef {Object} BufferInfo
	 * @property {number} numElements The number of elements to pass to `gl.drawArrays` or `gl.drawElements`.
	 * @property {WebGLBuffer} [indices] The indices `ELEMENT_ARRAY_BUFFER` if any indices exist.
	 * @property {Object.<string, module:webgl-utils.AttribInfo>} attribs The attribs approriate to call `setAttributes`
	 * @memberOf module:webgl-utils
	 */


	/**
	 * Creates a BufferInfo from an object of arrays.
	 *
	 * This can be passed to {@link module:webgl-utils.setBuffersAndAttributes} and to
	 * {@link module:webgl-utils:drawBufferInfo}.
	 *
	 * Given an object like
	 *
	 *     let arrays = {
	 *       position: { numComponents: 3, data: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0], },
	 *       texcoord: { numComponents: 2, data: [0, 0, 0, 1, 1, 0, 1, 1],                 },
	 *       normal:   { numComponents: 3, data: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],     },
	 *       indices:  { numComponents: 3, data: [0, 1, 2, 1, 2, 3],                       },
	 *     };
	 *
	 *  Creates an BufferInfo like this
	 *
	 *     bufferInfo = {
	 *       numElements: 4,        // or whatever the number of elements is
	 *       indices: WebGLBuffer,  // this property will not exist if there are no indices
	 *       attribs: {
	 *         a_position: { buffer: WebGLBuffer, numComponents: 3, },
	 *         a_normal:   { buffer: WebGLBuffer, numComponents: 3, },
	 *         a_texcoord: { buffer: WebGLBuffer, numComponents: 2, },
	 *       },
	 *     };
	 *
	 *  The properties of arrays can be JavaScript arrays in which case the number of components
	 *  will be guessed.
	 *
	 *     let arrays = {
	 *        position: [0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0],
	 *        texcoord: [0, 0, 0, 1, 1, 0, 1, 1],
	 *        normal:   [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],
	 *        indices:  [0, 1, 2, 1, 2, 3],
	 *     };
	 *
	 *  They can also by TypedArrays
	 *
	 *     let arrays = {
	 *        position: new Float32Array([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]),
	 *        texcoord: new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]),
	 *        normal:   new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]),
	 *        indices:  new Uint16Array([0, 1, 2, 1, 2, 3]),
	 *     };
	 *
	 *  Or augmentedTypedArrays
	 *
	 *     let positions = createAugmentedTypedArray(3, 4);
	 *     let texcoords = createAugmentedTypedArray(2, 4);
	 *     let normals   = createAugmentedTypedArray(3, 4);
	 *     let indices   = createAugmentedTypedArray(3, 2, Uint16Array);
	 *
	 *     positions.push([0, 0, 0, 10, 0, 0, 0, 10, 0, 10, 10, 0]);
	 *     texcoords.push([0, 0, 0, 1, 1, 0, 1, 1]);
	 *     normals.push([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
	 *     indices.push([0, 1, 2, 1, 2, 3]);
	 *
	 *     let arrays = {
	 *        position: positions,
	 *        texcoord: texcoords,
	 *        normal:   normals,
	 *        indices:  indices,
	 *     };
	 *
	 * For the last example it is equivalent to
	 *
	 *     let bufferInfo = {
	 *       attribs: {
	 *         a_position: { numComponents: 3, buffer: gl.createBuffer(), },
	 *         a_texcoods: { numComponents: 2, buffer: gl.createBuffer(), },
	 *         a_normals: { numComponents: 3, buffer: gl.createBuffer(), },
	 *       },
	 *       indices: gl.createBuffer(),
	 *       numElements: 6,
	 *     };
	 *
	 *     gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_position.buffer);
	 *     gl.bufferData(gl.ARRAY_BUFFER, arrays.position, gl.STATIC_DRAW);
	 *     gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_texcoord.buffer);
	 *     gl.bufferData(gl.ARRAY_BUFFER, arrays.texcoord, gl.STATIC_DRAW);
	 *     gl.bindBuffer(gl.ARRAY_BUFFER, bufferInfo.attribs.a_normal.buffer);
	 *     gl.bufferData(gl.ARRAY_BUFFER, arrays.normal, gl.STATIC_DRAW);
	 *     gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufferInfo.indices);
	 *     gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, arrays.indices, gl.STATIC_DRAW);
	 *
	 * @param {WebGLRenderingContext} gl A WebGLRenderingContext
	 * @param {Object.<string, array|object|typedarray>} arrays Your data
	 * @param {Object.<string, string>} [opt_mapping] an optional mapping of attribute to array name.
	 *    If not passed in it's assumed the array names will be mapped to an attribute
	 *    of the same name with "a_" prefixed to it. An other words.
	 *
	 *        let arrays = {
	 *           position: ...,
	 *           texcoord: ...,
	 *           normal:   ...,
	 *           indices:  ...,
	 *        };
	 *
	 *        bufferInfo = createBufferInfoFromArrays(gl, arrays);
	 *
	 *    Is the same as
	 *
	 *        let arrays = {
	 *           position: ...,
	 *           texcoord: ...,
	 *           normal:   ...,
	 *           indices:  ...,
	 *        };
	 *
	 *        let mapping = {
	 *          a_position: "position",
	 *          a_texcoord: "texcoord",
	 *          a_normal:   "normal",
	 *        };
	 *
	 *        bufferInfo = createBufferInfoFromArrays(gl, arrays, mapping);
	 *
	 * @return {module:webgl-utils.BufferInfo} A BufferInfo
	 * @memberOf module:webgl-utils
	 */
	public static createBufferInfoFromArrays(gl: WebGLRenderingContext, arrays: any, opt_mapping?:any):{attribs: any,
		indices: any,
		numElements: number
	} {
		let bufferInfo = {
			attribs: this.createAttribsFromArrays(gl, arrays, opt_mapping),
			indices: undefined,
			numElements: undefined
		};
		let indices = arrays.indices;
		if (indices) {
			indices = this.makeTypedArray(indices, 'indices');
			bufferInfo.indices = this.createBufferFromTypedArray(gl, indices, gl.ELEMENT_ARRAY_BUFFER);
			bufferInfo.numElements = indices.length;
		} else {
			bufferInfo.numElements = this.getNumElementsFromNonIndexedArrays(arrays);
		}

		return bufferInfo;
	}

	/**
	 * Creates buffers from typed arrays
	 *
	 * Given something like this
	 *
	 *     let arrays = {
	 *        positions: [1, 2, 3],
	 *        normals: [0, 0, 1],
	 *     }
	 *
	 * returns something like
	 *
	 *     buffers = {
	 *       positions: WebGLBuffer,
	 *       normals: WebGLBuffer,
	 *     }
	 *
	 * If the buffer is named 'indices' it will be made an ELEMENT_ARRAY_BUFFER.
	 *
	 * @param {WebGLRenderingContext} gl A WebGLRenderingContext.
	 * @param {Object<string, array|typedarray>} arrays
	 * @return {Object<string, WebGLBuffer>} returns an object with one WebGLBuffer per array
	 * @memberOf module:webgl-utils
	 */
	public static createBuffersFromArrays(gl, arrays) {
		const buffers = {
			numElements: undefined,
		};
		Object.keys(arrays).forEach(function (key) {
			const type = key === 'indices' ? gl.ELEMENT_ARRAY_BUFFER : gl.ARRAY_BUFFER;
			const array = WebGLUtil.makeTypedArray(arrays[key]);
			buffers[key] = WebGLUtil.createBufferFromTypedArray(gl, array, type);
		});

		// hrm
		if (arrays.indices) {
			buffers.numElements = arrays.indices.length;
		} else if (arrays.position) {
			buffers.numElements = arrays.position.length / 3;
		}

		return buffers;
	}

	/**
   * Calls `gl.drawElements` or `gl.drawArrays`, whichever is appropriate
   *
   * normally you'd call `gl.drawElements` or `gl.drawArrays` yourself
   * but calling this means if you switch from indexed data to non-indexed
   * data you don't have to remember to update your draw call.
   *
   * @param {WebGLRenderingContext} gl A WebGLRenderingContext
   * @param {module:webgl-utils.BufferInfo} bufferInfo as returned from createBufferInfoFromArrays
   * @param {enum} [primitiveType] eg (gl.TRIANGLES, gl.LINES, gl.POINTS, gl.TRIANGLE_STRIP, ...)
   * @param {number} [count] An optional count. Defaults to bufferInfo.numElements
   * @param {number} [offset] An optional offset. Defaults to 0.
   * @memberOf module:webgl-utils
   */
	public static drawBufferInfo(gl, bufferInfo, primitiveType?:number, count?:number, offset?:number) {
		const indices = bufferInfo.indices;
		primitiveType = primitiveType === undefined ? gl.TRIANGLES : primitiveType;
		const numElements = count === undefined ? bufferInfo.numElements : count;
		offset = offset === undefined ? 0 : offset;
		if (indices) {
			gl.drawElements(primitiveType, numElements, gl.UNSIGNED_SHORT, offset);
		} else {
			gl.drawArrays(primitiveType, offset, numElements);
		}
	}

}